Процессор в процессе развития научился на работать на значительно большей частоте чем память на конденсаторах, например частота процессора может достигать 2.2Ггц пока память всего 300МГц. По этой причине на шине памяти ставят буфер ввода вывода, который может в один такт памяти вкладывать несколько команд и запоминать возвраты себе в память. Такой подход позволяет ускорить работу с памятью в несколько раз, что мало, но хоть что то
Старая архитектура расположения памяти и процессора
В новых процессорах северный мост для быстродействия вкладывается в сам ЦП
DRAM (ОЗУ) | 1 конденсатор + 1 транзистор | Дешёвая, плотная, требует рефреша |
SRAM (кэш) | 6 транзисторов | Очень быстрая, дорогая, не требует рефреша |
Драм организована как набор банков, в рамках одного такта можно вычитывать данныек параллельно из каждого банка. Один банк представляет из себя квадрат. В рамках одного такта идут два сигнала на вычитывание строки и столбца у банка. Деление на квадратные банки необходимо для уменьшения числа проводов, так, если бы все ячейки были бы в линию, то длина контакта была бы значительно больше.
При чтении данных из банка, сначала происходит включение конкретной строки, это долго. По алгоритму доступа, сначала идет команда по открытию строки, и факт открытия строки сохраняется, и только после этого можно быстро считывать и выполнять команды на чтение любой ячейки из уже открытой строки.
*По этой причине значительно быстрее последовательный доступ к памяти чем случайный, тк на открытие новой строки тратиться много времени. (Одна строка обычно несколько кб, в большинстве современных компов 8кб)
ISA – это контракт (интерфейс), заключенный между производителем процессора и пользователями: программы пишутся в соответствии с контрактом. Этот слой мы видим, например, при программировании на языке ассемблера для данной архитектуры
В процессоре присутствует дополнительная очень быстрая память SRAM. Это кэш в который он может обращаться для ускорения. Для того чтобы максимизировать число использований кэша применяются следующие локализации данных:
Кэш это небольшой участок памяти и важно заметить, что данные между DRAM и кэшом перемещаются только целыми блоками. Такой блок называется строка кэша и обычно она размером 64 байта. Это значит, даже если нужно прочитать только один бит, будет прочитано и записано в кэш 64 байта
Однако! Стоит заметить, что процессор общается с ОЗУ машинными словами в 8 байт раз в такт, а значит на чтение любого бита потребуется 8 тактов! Для решения этой проблемы применяют прием, когда из ОЗУ читается первым делом машинное слово содержащее запрашиваемый бит, программа продолжает выполнение, а кэш докручивает оставшиеся машинные слова асинхронно.
ПРИМЕЧАНИЕ Как выглядит типичный паттерн доступа к памяти? Если кто-то хочет прочитать данные из памяти, то в кеше создается соответствующая строка и в нее читается 64 байта. Если кто-то хочет записать данные в память, то первый шаг точно такой же – заполняется строка кеша, если ее не было раньше. И модифицируются именно эти кешированные данные. Далее возможны две стратегии. При сквозной записи только что модифицированные в кеше данные сразу же сохраняются в основной памяти. Этот подход просто реализовать, но он создает большую нагрузку на шину памяти. При отложенной записи модифицированная строка помечается флагом. Когда в кеше не останется места для новых данных, этот помеченный блок будет записан в основную память и удален из кеша. Процессор может выгружать такие блоки периодически, когда сочтет необходимым (например, во время простоя). Есть еще один вид оптимизации, который называется комбинированной записью. Он гарантирует, что данная строка кеша из данной области памяти записывается целиком (а не отдельными словами). Здесь снова используется тот факт, что последовательный доступ к памяти быстрее.
Это еще одна причина почему последовательный доступ к памяти значительно быстрее, в последовательном доступе к памяти мы вычитываем сразу 64 байта и работаем с ними, а в случае когда мы вычитываем данные рандомно, мы вычитываем данные, занимаем кэш, ждем пока кэш заполнится, а потом снова вызываем чтение 64 байтов, берем часть данных и снова ждем.
Иногда необходимо записать в память сразу большой массив данных, который понадобиться нескоро, в таком случае мы будем писать в кэш данные чтобы потом переписать их в DRAM, а потом снова безцельно писать данные в кэш. Это не эффективно. В ассемблере и плюсах есть функции позволяющие процессору писать данные в DRAM в обход кэша.
С развитием процессоров усложнилась и работа с кэшами, теперь в процессорах их обычно существует 3 уровня L1 L2 L3. Каждый следующий чуть больше и чуть медленнее. Также L1 разбит на два куска, один для именно команд процессора, второй для данных.
Таблица сравнения кэшей:
Область | Задержка | Размер |
---|---|---|
L1 | < 2,0 нс | 64КиБ (32+32) |
L2 | 4,8 нс | 256КиБ |
L3 | 14,4 нс | 8МиБ |
ОЗУ | 71,4 нс | - |
Диск | 150 000 нс | - |
Пример, программа считывает данные из массива, где один элемента массива это структура равная 64байта, то есть одной кэш строке.
Как можно заметить, когда размер массива превышает размер текущей области кэша, начинает использоваться больший кэш и скорость резко проседает
В современных компьютерах, ЦП содержат несколько ядер, где каждое ядро может выполнять параллельно свой поток (Более того, современные ядра реализуют механизм при котором одно ядро может одновременно и паралельно выполнять работу нескольких потоков, например двух). Представлена схема распределения кэша между ядрами
Стоит заметить, что если ядро выполняет два потока одновременно, то кэш фактически делится пополам между потоками.
Теперь встает вопрос, а ведь куча может быть одна на несколько потоков, в таком случае кэши могут становиться недействительны если два потока закэшировали изменили одну и туже строку кэша. На такой случай в ядре реализован механизм оповещений о изменении конкретной строки кэша, в таком случае все остальные ядра считают свою версию кэша не валидной и идут за новой.
Из-за этого механизма стоит избегать одновременного использования одной и той же строки кэша разными потоками, это по сути обнуляет работу кэша и заставляет перекопировать кэш.
ОС и ЦП в связке, для процессов создает иллюзию что каждый процесс существует отдельно в своей среде со своим адресным пространством. Но при этом, для ОС существует потребность в отображении памяти из виртуального адресного пространства на пространство железное. Было бы неудобно отображать отдельно каждый байт, поэтому ОС применяет страницы. Страница это участки непрерывной памяти одного размера и они являются основной единицей при управлении участками виртуальной памяти.
Кроме того, для каждого процесса ОС хранит таблицу страниц, которая позволяет отобразить виртуальный адрес в физический.
Раньше применялась одноуровневая таблица страниц, в таком случае адрес байта состоял из селектора страницы (то есть адреса в самой таблице) и смещения внутри самой страницы
В 64 разрядных компьютерах возникла проблема, что размер страницы 4кб менять не хочется, иначе будет память пустовать. Но при этом увеличившиеся адреса с 32 до 64 бит дают возможность увеличить селектор. Но при увеличении селектора надо бы и таблицу страниц увеличить, и тут гвоздь, такое увеличение селектора породит увеличение самой таблицы до 512гб и это для каждого процесса!
По этой причине, систему адресации модернизировали, теперь применяется сразу несколько таблиц, но это вызывает проблему, что теперь поиск нужной страницы в памяти - долгая операция
Поэтому был введен буфер ассоциативной трансляции (Translation Look-Aside Buffer – TLB), который кеширует сам процесс трансляции. Идея проста – TLB работает как отображение, в котором селектор является ключом, а адрес физической страницы – значением.
Если ты вызываешь страницу которой еще нет в TLP то приходиться запускать долгий поиск адреса необходимой страницы.
Подсистема управления памятью в Windows представлена двумя основными уровнями:
Для управления зарезервированными и частными страницами применяется вызов функций VirtualAlloc/VirtualFree и VirtualLock/VirtualUnlock
Обращение к свободной странице и к просто зарезервированной памяти влечет исключение нарушения доступа, потому что эту память еще нельзя отобразить на физическую.
Используя эти состояния страниц Windows выделяет следующие области в памяти процесса:
При выделении страницы без этапа резервирования, процесс резирвирования просто пройдет под капотом
Процесс содержит одну кучу по умолчанию, в основном она применяется для целей самого Windows. Также процесс может создать сколько ему хочется дополнительных куч используя Heap API. Пример такой создаваемой дополнительной кучи это среда выполнения Microsoft C, которая создает такую дополнительную кучу для запускаемого c++ и c кода.
В целом, если крупными мазками, то процесс .net приложения содержит следующие зоны
Важный момент, существует механизм рандомизации структуры адресного пространства (Address Space Layout Randomization – ASLR), из-за которого данная схема всего лишь схема. Данный механизм нужен для безопасности и он подразумевает рандомную перестановку по виртуальной памяти больших структур, то есть различных куч, стеков итд.
Как и в Windows, размер страницы 4кб
Ограничения на размер адресного пространства также довольно схожи
В отличие от Windows страница может находиться в одном из трех состояний
Обобщая, у процесса память имеет следующие аля зоны
Если в Windows гранулярность выделения памяти равна 64 КБ, то в Linux это просто размер страницы, который чаще всего равен 4 КБ
Для управления страницами в памяти существуют следующие функции
Тк в Linux нет резервирования участков памяти, возникает проблема при работе со стеком, в таких условиях при росте стека, виртуальных адресов для роста может не остаться и придется его разделить.
В Linux часто вместо резервирования делают большие участки памяти частными, но с нулевыми правами доступа, в таком случае все знают что эту память трогать не надо. При этом есть возможность освобождать доступ кусочками, постранично.